/*-------------------------------------------------------------------------
 *
 * 前端代码的字符串处理实用例程
 *
 * 各种实用函数在构造SQL查询和解释后端输出时非常有用。
 *
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * src/fe_utils/string_utils.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres_fe.h"

#include <ctype.h>

#include "common/keywords.h"
#include "fe_utils/string_utils.h"
#include "mb/pg_wchar.h"

static PQExpBuffer fc_defaultGetLocalPQExpBuffer(void);

/* 由此文件导出的全局变量 */
int			quote_all_identifiers = 0;
PQExpBuffer (*getLocalPQExpBuffer) (void) = fc_defaultGetLocalPQExpBuffer;

static int	fmtIdEncoding = -1;


/*
 * 返回一个临时的 PQExpBuffer，直到下次调用该函数为止。
 * 这用于 fmtId 和 fmtQualifiedId。
 *
 * 非重入且不线程安全，但减少内存泄漏。您可以
 * 通过设置 getLocalPQExpBuffer
 * 函数指针来替换为自定义版本。
 */
static PQExpBuffer fc_defaultGetLocalPQExpBuffer(void)
{
	static PQExpBuffer fc_id_return = NULL;

	if (fc_id_return)				/* 第一次通过？ */
	{
		/* 相同的缓冲区，仅清空内容 */
		resetPQExpBuffer(fc_id_return);
	}
	else
	{
		/* 新缓冲区 */
		fc_id_return = createPQExpBuffer();
	}

	return fc_id_return;
}

/*
 * 设置 fmtId() 和 fmtQualifiedId() 使用的编码。
 *
 * 这对多个连接具有不同编码的情况不安全，
 * 但没有真正的其他方法来解决需要知道
 * fmtId()/fmtQualifiedId() 输入的编码以安全转义的问题。最终我们应该去掉 fmtId()。
 */
void setFmtEncoding(int fc_encoding)
{
	fmtIdEncoding = fc_encoding;
}

/*
 * 返回 fmtId() 和 fmtQualifiedId() 当前配置的编码。
 */
static int fc_getFmtEncoding(void)
{
	if (fmtIdEncoding != -1)
		return fmtIdEncoding;

	/*
	 * 在断言构建中，如果编码未设置，似乎最好直接失败，
	 * 以便更容易找到缺少调用的地方。但在
	 * 生产构建中，这似乎是个坏主意，因此我们反而默认使用 UTF-8。
	 */
	Assert(fmtIdEncoding != -1);

	return PG_UTF8;
}

/*
 *	如果输入字符串不作为合法的 SQL 标识符，则对其进行引用。
 *
 *	注意，返回的字符串必须在再次调用 fmtIdEnc 之前使用，
 *	因为我们每次都重用相同的返回缓冲区。
 */
const char * fmtIdEnc(const char *fc_rawid, int fc_encoding)
{
	PQExpBuffer fc_id_return = getLocalPQExpBuffer();

	const char *fc_cp;
	bool		fc_need_quotes = false;
	size_t		fc_remaining = strlen(fc_rawid);

	/*
	 * 这些检查需要匹配 scan.l 中的标识符生成。不要
	 * 使用 islower() 等等。
	 */
	if (quote_all_identifiers)
		fc_need_quotes = true;
	/* 第一个字符的规则稍有不同 */
	else if (!((fc_rawid[0] >= 'a' && fc_rawid[0] <= 'z') || fc_rawid[0] == '_'))
		fc_need_quotes = true;
	else
	{
		/* 否则检查整个字符串 */
		fc_cp = fc_rawid;
		for (size_t fc_i = 0; fc_i < fc_remaining; fc_i++, fc_cp++)
		{
			if (!((*fc_cp >= 'a' && *fc_cp <= 'z')
				  || (*fc_cp >= '0' && *fc_cp <= '9')
				  || (*fc_cp == '_')))
			{
				fc_need_quotes = true;
				break;
			}
		}
	}

	if (!fc_need_quotes)
	{
		/*
		 * 检查是否为关键字。我们引用关键字，除了未保留的那些。
		 * （在某些情况下，我们可以避免引用 col_name 或 type_func_name
		 * 关键字，但判断这点似乎要复杂得多。）
		 *
		 * 注意：ScanKeywordLookup() 进行不区分大小写的比较，但
		 * 这没问题，因为我们已经知道这里都是小写。
		 */
		int			fc_kwnum = ScanKeywordLookup(fc_rawid, &ScanKeywords);

		if (fc_kwnum >= 0 && ScanKeywordCategories[fc_kwnum] != UNRESERVED_KEYWORD)
			fc_need_quotes = true;
	}

	if (!fc_need_quotes)
	{
		/* 不需要引用 */
		appendPQExpBufferStr(fc_id_return, fc_rawid);
	}
	else
	{
		appendPQExpBufferChar(fc_id_return, '"');

		fc_cp = &fc_rawid[0];
		while (fc_remaining > 0)
		{
			int			fc_charlen;

			/* 针对普通 ASCII 的快速路径 */
			if (!IS_HIGHBIT_SET(*fc_cp))
			{
				/*
				 * 我们在字符串中找到了双引号吗？那么将其变为
				 * 按照 SQL99 规则的双重双引号。在此之前，我们放入了一个
				 * 反斜杠/双引号对。- thomas 2000-08-05
				 */
				if (*fc_cp == '"')
					appendPQExpBufferChar(fc_id_return, '"');
				appendPQExpBufferChar(fc_id_return, *fc_cp);
				fc_remaining--;
				fc_cp++;
				continue;
			}

			/* 可能多字节字符的慢路径 */
			fc_charlen = pg_encoding_mblen(fc_encoding, fc_cp);

			if (fc_remaining < fc_charlen)
			{
				/*
				 * 如果字符的长度超过可用输入，
				 * 用无效序列替换字符串。无效
				 * 序列确保转义字符串将触发
				 * 服务器端的错误，即使我们无法直接报告
				 * 这里的错误。
				 */
				enlargePQExpBuffer(fc_id_return, 2);
				pg_encoding_set_invalid(fc_encoding,
										fc_id_return->data + fc_id_return->len);
				fc_id_return->len += 2;
				fc_id_return->data[fc_id_return->len] = '\0';

				/* 没有更多输入数据，所以我们可以停止 */
				break;
			}
			else if (pg_encoding_verifymbchar(fc_encoding, fc_cp, fc_charlen) == -1)
			{
				/*
				 * 多字节字符无效。重要的是要验证
				 * 无效的多字节字符可能被用来例如“跳过”引号字符，例如在逐字符解析时。
				 *
				 * 将与无效字符对应的字节替换为无效序列，原因同上。
				 *
				 * 在第一次遇到高位集合时验证整个字符串会快一些，但这样我们可以
				 * 仅替换无效字符，这可能使用户更容易找到更大字符串中的无效编码部分。
				 */
				enlargePQExpBuffer(fc_id_return, 2);
				pg_encoding_set_invalid(fc_encoding,
										fc_id_return->data + fc_id_return->len);
				fc_id_return->len += 2;
				fc_id_return->data[fc_id_return->len] = '\0';

				/*
				 * 复制无效多字节字符之后的字符串其余部分。
				 */
				fc_remaining -= fc_charlen;
				fc_cp += fc_charlen;
			}
			else
			{
				for (int fc_i = 0; fc_i < fc_charlen; fc_i++)
				{
					appendPQExpBufferChar(fc_id_return, *fc_cp);
					fc_remaining--;
					fc_cp++;
				}
			}
		}

		appendPQExpBufferChar(fc_id_return, '"');
	}

	return fc_id_return->data;
}

/*
 * 如果输入字符串不是合法的 SQL 标识符，则对其进行引用。
 *
 * 请注意，返回的字符串必须在再次调用 fmtId 之前使用，
 * 因为我们每次都重用相同的返回缓冲区。
 *
 * 备注：这假定之前已调用 setFmtEncoding() 以配置 rawid 的编码。
 * 使用具有显式编码的 fmtIdEnc() 更好。
 */
const char * fmtId(const char *fc_rawid)
{
	return fmtIdEnc(fc_rawid, fc_getFmtEncoding());
}

/*
 * fmtQualifiedIdEnc - 构造一个模式合格名称，并根据需要进行引用。
 *
 * 像 fmtId 一样，在再次调用之前使用结果。
 *
 * 因为我们调用 fmtId 并且它也使用 getLocalPQExpBuffer()，所以在完成调用 fmtId() 之前不能使用那个缓冲区。
 */
const char * fmtQualifiedIdEnc(const char *fc_schema, const char *fc_id, int fc_encoding)
{
	PQExpBuffer fc_id_return;
	PQExpBuffer fc_lcl_pqexp = createPQExpBuffer();

	/* 一些调用者可能未能提供模式名称 */
	if (fc_schema && *fc_schema)
	{
		appendPQExpBuffer(fc_lcl_pqexp, "%s.", fmtIdEnc(fc_schema, fc_encoding));
	}
	appendPQExpBufferStr(fc_lcl_pqexp, fmtIdEnc(fc_id, fc_encoding));

	fc_id_return = getLocalPQExpBuffer();

	appendPQExpBufferStr(fc_id_return, fc_lcl_pqexp->data);
	destroyPQExpBuffer(fc_lcl_pqexp);

	return fc_id_return->data;
}

/*
 * fmtQualifiedId - 构造一个模式合格名称，并根据需要进行引用。
 *
 * 像 fmtId 一样，在再次调用之前使用结果。
 *
 * 因为我们调用 fmtId 并且它也使用 getLocalPQExpBuffer()，所以在完成调用 fmtId() 之前不能使用那个缓冲区。
 *
 * 备注：这假定之前已调用 setFmtEncoding() 以配置模式/id 的编码。
 * 使用具有显式编码的 fmtQualifiedIdEnc() 更好。
 */
const char * fmtQualifiedId(const char *fc_schema, const char *fc_id)
{
	return fmtQualifiedIdEnc(fc_schema, fc_id, fc_getFmtEncoding());
}


/*
 * 将 Postgres 版本号（由 PQserverVersion() 返回的 PG_VERSION_NUM 整数格式）
 * 格式化为字符串。主要存在是为了
 * 封装有关两部分与三部分版本号的知识。
 *
 * 为了可重入性，调用者必须提供字符串放置的缓冲区。
 * 推荐的缓冲区大小为 32 字节。
 *
 * 返回 'buf' 的地址，作为一种表示便利。
 */
char * formatPGVersionNumber(int fc_version_number, bool fc_include_minor,
					  char *fc_buf, size_t fc_buflen)
{
	if (fc_version_number >= 100000)
	{
		/* 新的两部分样式 */
		if (fc_include_minor)
			snprintf(fc_buf, fc_buflen, "%d.%d", fc_version_number / 10000,
					 fc_version_number % 10000);
		else
			snprintf(fc_buf, fc_buflen, "%d", fc_version_number / 10000);
	}
	else
	{
		/* 旧的三部分样式 */
		if (fc_include_minor)
			snprintf(fc_buf, fc_buflen, "%d.%d.%d", fc_version_number / 10000,
					 (fc_version_number / 100) % 100,
					 fc_version_number % 100);
		else
			snprintf(fc_buf, fc_buflen, "%d.%d", fc_version_number / 10000,
					 (fc_version_number / 100) % 100);
	}
	return fc_buf;
}


/*
 * 将字符串值转换为 SQL 字符串文字并将其附加到
 * 给定的缓冲区。我们假设指定的 client_encoding 和
 * standard_conforming_strings 设置。
 *
 * 这基本上等价于 libpq 的 PQescapeStringInternal，
 * 除了输出缓冲区结构。我们在没有可用 PGconn 的情况下需要它。
 * 如果有的话，appendStringLiteralConn 是更好的选择。
 */
void appendStringLiteral(PQExpBuffer fc_buf, const char *fc_str,
					int fc_encoding, bool fc_std_strings)
{
	size_t		fc_length = strlen(fc_str);
	const char *fc_source = fc_str;
	char	   *fc_target;
	size_t		fc_remaining = fc_length;

	if (!enlargePQExpBuffer(fc_buf, 2 * fc_length + 2))
		return;

	fc_target = fc_buf->data + fc_buf->len;
	*fc_target++ = '\'';

	while (fc_remaining > 0)
	{
		char		fc_c = *fc_source;
		int			fc_charlen;
		int			fc_i;

		/* 针对普通 ASCII 的快速路径 */
		if (!IS_HIGHBIT_SET(fc_c))
		{
			/* 如有需要则应用引用 */
			if (SQL_STR_DOUBLE(fc_c, !fc_std_strings))
				*fc_target++ = fc_c;
			/* 复制字符 */
			*fc_target++ = fc_c;
			fc_source++;
			fc_remaining--;
			continue;
		}

		/* 可能多字节字符的慢路径 */
		fc_charlen = PQmblen(fc_source, fc_encoding);

		if (fc_remaining < fc_charlen)
		{
			/*
			 * 如果字符长度超过可用输入，将字符串替换为无效序列。
			 * 无效序列确保转义字符串将在服务器端触发错误，
			 * 即使我们无法在这里直接报告错误。
			 *
			 * 我们知道无效序列有足够的空间，因为“目标”缓冲区
			 * 的长度是 2 * length + 2，并且在最坏的情况下，
			 * 我们将单个输入字节替换为两个无效字节。
			 */
			pg_encoding_set_invalid(fc_encoding, fc_target);
			fc_target += 2;

			/* 没有更多有效的输入数据，所以我们可以停止 */
			break;
		}
		else if (pg_encoding_verifymbchar(fc_encoding, fc_source, fc_charlen) == -1)
		{
			/*
			 * 多字节字符无效。 验证这一点很重要，因为无效的多字节字符
			 * 可能会用于“跳过”引号字符，例如在逐字符解析时。
			 *
			 * 用无效序列替换与无效字符对应的字节，原因同上。
			 *
			 * 在我们第一次遇到高位集时，验证整个字符串会快一些，
			 * 但这样我们可以仅替换无效字符，这可能使用户更容易
			 * 找到较大字符串中无效编码的部分。
			 */
			pg_encoding_set_invalid(fc_encoding, fc_target);
			fc_target += 2;
			fc_remaining -= fc_charlen;

			/*
			 * 复制无效多字节字符后的其余字符串。
			 */
			fc_source += fc_charlen;
		}
		else
		{
			/* 复制字符 */
			for (fc_i = 0; fc_i < fc_charlen; fc_i++)
			{
				*fc_target++ = *fc_source++;
				fc_remaining--;
			}
		}
	}

	/* 写入结束引号和 NUL 字符。 */
	*fc_target++ = '\'';
	*fc_target = '\0';

	fc_buf->len = fc_target - fc_buf->data;
}


/*
 * 将字符串值转换为 SQL 字符串字面量，并将其附加到
 * 给定缓冲区。 编码和字符串语法规则由当前 PGconn 的设置指示。
 */
void appendStringLiteralConn(PQExpBuffer fc_buf, const char *fc_str, PGconn *fc_conn)
{
	size_t		fc_length = strlen(fc_str);

	/*
	 * XXX 这是一个小聪明，用于消除我们实用程序中的 escape_string_warning。
	 * 它总有一天会消失。
	 */
	if (strchr(fc_str, '\\') != NULL && PQserverVersion(fc_conn) >= 80100)
	{
		/* 确保我们没有与标识符相邻 */
		if (fc_buf->len > 0 && fc_buf->data[fc_buf->len - 1] != ' ')
			appendPQExpBufferChar(fc_buf, ' ');
		appendPQExpBufferChar(fc_buf, ESCAPE_STRING_SYNTAX);
		appendStringLiteral(fc_buf, fc_str, PQclientEncoding(fc_conn), false);
		return;
	}
	/* XXX 结束小聪明 */

	if (!enlargePQExpBuffer(fc_buf, 2 * fc_length + 2))
		return;
	appendPQExpBufferChar(fc_buf, '\'');
	fc_buf->len += PQescapeStringConn(fc_conn, fc_buf->data + fc_buf->len,
								   fc_str, fc_length, NULL);
	appendPQExpBufferChar(fc_buf, '\'');
}


/*
 * 将字符串值转换为美元引用字面量，并将其附加到
 * 给定缓冲区。如果 dqprefix 参数不为 NULL，则美元引号分隔符
 * 将以该参数开始（在开头 $ 之后）。
 *
 * str 完全不进行转义，以符合解析美元引用字符串的规则。
 * 此外，我们无需担心编码问题。
 */
void appendStringLiteralDQ(PQExpBuffer fc_buf, const char *fc_str, const char *fc_dqprefix)
{
	static const char fc_suffixes[] = "_XXXXXXX";
	int			fc_nextchar = 0;
	PQExpBuffer fc_delimBuf = createPQExpBuffer();

	/* 如果不为 NULL，则以 $ + dqprefix 开始 */
	appendPQExpBufferChar(fc_delimBuf, '$');
	if (fc_dqprefix)
		appendPQExpBufferStr(fc_delimBuf, fc_dqprefix);

	/*
	 * 确保我们选择的分隔符在引用的字符串中（除了尾随的 $ 之外）不存在。
	 * 我们不检查尾随的 $，因为以 $foo 结尾的字符串不能用 $foo$ 引用。
	 */
	while (strstr(fc_str, fc_delimBuf->data) != NULL)
	{
		appendPQExpBufferChar(fc_delimBuf, fc_suffixes[fc_nextchar++]);
		fc_nextchar %= sizeof(fc_suffixes) - 1;
	}

	/* 添加尾随 $ */
	appendPQExpBufferChar(fc_delimBuf, '$');

	/* 引用它，我们就完成了 */
	appendPQExpBufferStr(fc_buf, fc_delimBuf->data);
	appendPQExpBufferStr(fc_buf, fc_str);
	appendPQExpBufferStr(fc_buf, fc_delimBuf->data);

	destroyPQExpBuffer(fc_delimBuf);
}


/*
 * 将 bytea 值（以原始字节形式呈现）转换为 SQL 字符串字面量
 * 并将其附加到给定缓冲区。 我们假设指定了
 * standard_conforming_strings 设置。
 *
 * 在没有可用 PGconn 的情况下需要此操作。
 * 在有的情况下，PQescapeByteaConn 是更好的选择。
 */
void appendByteaLiteral(PQExpBuffer fc_buf, const unsigned char *fc_str, size_t fc_length,
				   bool fc_std_strings)
{
	const unsigned char *fc_source = fc_str;
	char	   *fc_target;

	static const char fc_hextbl[] = "0123456789abcdef";

	/*
	 * 这个实现被硬编码为生成十六进制格式输出。我们不知道输出将加载到哪个服务器版本中，因此无法做出智能的格式选择。始终使用旧的转义格式可能更好。
	 */
	if (!enlargePQExpBuffer(fc_buf, 2 * fc_length + 5))
		return;

	fc_target = fc_buf->data + fc_buf->len;
	*fc_target++ = '\'';
	if (!fc_std_strings)
		*fc_target++ = '\\';
	*fc_target++ = '\\';
	*fc_target++ = 'x';

	while (fc_length-- > 0)
	{
		unsigned char fc_c = *fc_source++;

		*fc_target++ = fc_hextbl[(fc_c >> 4) & 0xF];
		*fc_target++ = fc_hextbl[fc_c & 0xF];
	}

	/* 写入结束引号和 NUL 字符。 */
	*fc_target++ = '\'';
	*fc_target = '\0';

	fc_buf->len = fc_target - fc_buf->data;
}


/*
 * 将给定字符串附加到正在缓冲区中构建的 shell 命令中，根据需要进行 shell 风格的引用，以准确生成一个参数。
 *
 * 禁止 LF 或 CR 字符，因为这些字符在设计安全漏洞之外几乎没有实际用途。Windows 命令 shell 作为包含 LF 或 CR 字符的参数的渠道是不可用的。未来的主要版本应该在 CREATE ROLE 和 CREATE DATABASE 中拒绝这些字符，因为在那里使用最终导致这里的错误。
 *
 * appendShellString() 在出现 LF 或 CR 时简单地打印错误并终止。appendShellStringNoError() 从结果中省略这些字符，如果有的话返回 false。
 */
void appendShellString(PQExpBuffer fc_buf, const char *fc_str)
{
	if (!appendShellStringNoError(fc_buf, fc_str))
	{
		fprintf(stderr,
				_("shell command argument contains a newline or carriage return: \"%s\"\n"),
				fc_str);
		exit(EXIT_FAILURE);
	}
}

bool appendShellStringNoError(PQExpBuffer fc_buf, const char *fc_str)
{
#ifdef WIN32
	int			backslash_run_length = 0;
#endif
	bool		fc_ok = true;
	const char *fc_p;

	/*
	 * 如果字符串非空且显然只包含安全字符，就不要费心添加引号。
	 */
	if (*fc_str != '\0' &&
		strspn(fc_str, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_./:") == strlen(fc_str))
	{
		appendPQExpBufferStr(fc_buf, fc_str);
		return fc_ok;
	}

#ifndef WIN32
	appendPQExpBufferChar(fc_buf, '\'');
	for (fc_p = fc_str; *fc_p; fc_p++)
	{
		if (*fc_p == '\n' || *fc_p == '\r')
		{
			fc_ok = false;
			continue;
		}

		if (*fc_p == '\'')
			appendPQExpBufferStr(fc_buf, "'\"'\"'");
		else
			appendPQExpBufferChar(fc_buf, *fc_p);
	}
	appendPQExpBufferChar(fc_buf, '\'');
#else							/* WIN32 */

	/*
	 * Windows 系统() 参数经历两个层次的解释。
	 * 首先，cmd.exe 解释字符串。其行为没有文档记录，但插入符号会转义任何除了 LF 或 CR 之外的特殊字节。插入符号在 LF 或 CR 之前的处理在 "cmd.exe /c" 和其他模式之间有所不同，在这里不可用。
	 *
	 * 其次，新进程解析其命令行以构造 argv (请参阅 https://msdn.microsoft.com/en-us/library/17w5ykft.aspx)。这会特殊处理反斜杠-双引号序列。
	 */
	appendPQExpBufferStr(buf, "^\"");
	for (p = str; *p; p++)
	{
		if (*p == '\n' || *p == '\r')
		{
			ok = false;
			continue;
		}

		/* 将 N 个反斜杠更改为双引号前的 2N+1 个反斜杠。 */
		if (*p == '"')
		{
			while (backslash_run_length)
			{
				appendPQExpBufferStr(buf, "^\\");
				backslash_run_length--;
			}
			appendPQExpBufferStr(buf, "^\\");
		}
		else if (*p == '\\')
			backslash_run_length++;
		else
			backslash_run_length = 0;

		/*
		 * 拒绝对最普通的字符进行插入符号转义，以便于调试，并避免接近命令长度限制。
		 */
		if (!((*p >= 'a' && *p <= 'z') ||
			  (*p >= 'A' && *p <= 'Z') ||
			  (*p >= '0' && *p <= '9')))
			appendPQExpBufferChar(buf, '^');
		appendPQExpBufferChar(buf, *p);
	}

	/*
	 * 将参数末尾的 N 个反斜杠更改为 2N 个反斜杠，因为它们位于终止参数的双引号之前。
	 */
	while (backslash_run_length)
	{
		appendPQExpBufferStr(buf, "^\\");
		backslash_run_length--;
	}
	appendPQExpBufferStr(buf, "^\"");
#endif							/* WIN32 */

	return fc_ok;
}


/*
 * 将给定字符串附加到缓冲区，为将字符串作为 libpq 连接字符串中关键字/值对的值传递进行适当的引用。
 */
void appendConnStrVal(PQExpBuffer fc_buf, const char *fc_str)
{
	const char *fc_s;
	bool		fc_needquotes;

	/*
	 * 如果字符串是一个或多个普通的 ASCII 字符，则无需对其进行引用。这相当保守，但宁可安全也不要抱怨。
	 */
	fc_needquotes = true;
	for (fc_s = fc_str; *fc_s; fc_s++)
	{
		if (!((*fc_s >= 'a' && *fc_s <= 'z') || (*fc_s >= 'A' && *fc_s <= 'Z') ||
			  (*fc_s >= '0' && *fc_s <= '9') || *fc_s == '_' || *fc_s == '.'))
		{
			fc_needquotes = true;
			break;
		}
		fc_needquotes = false;
	}

	if (fc_needquotes)
	{
		appendPQExpBufferChar(fc_buf, '\'');
		while (*fc_str)
		{
			/* ' 和 \ 必须通过 \' 和 \\ 进行转义 */
			if (*fc_str == '\'' || *fc_str == '\\')
				appendPQExpBufferChar(fc_buf, '\\');

			appendPQExpBufferChar(fc_buf, *fc_str);
			fc_str++;
		}
		appendPQExpBufferChar(fc_buf, '\'');
	}
	else
		appendPQExpBufferStr(fc_buf, fc_str);
}


/*
 * 附加一个 psql 元命令，该命令使用当前连接的用户、主机和端口连接到给定数据库。
 */
void appendPsqlMetaConnect(PQExpBuffer fc_buf, const char *fc_dbname)
{
	const char *fc_s;
	bool fc_complex;

	/*
	 * 如果名称是普通的 ASCII 字符，发出简单的 "\connect "foo""。对于其他名称，即使许多名称在技术上不需要它，仍跳过到一般情况。没有数据库的名称是零长度的。
	 */
	fc_complex = false;

	for (fc_s = fc_dbname; *fc_s; fc_s++)
	{
		if (*fc_s == '\n' || *fc_s == '\r')
		{
			fprintf(stderr,
					_("database name contains a newline or carriage return: \"%s\"\n"),
					fc_dbname);
			exit(EXIT_FAILURE);
		}

		if (!((*fc_s >= 'a' && *fc_s <= 'z') || (*fc_s >= 'A' && *fc_s <= 'Z') ||
			  (*fc_s >= '0' && *fc_s <= '9') || *fc_s == '_' || *fc_s == '.'))
		{
			fc_complex = true;
		}
	}

	if (fc_complex)
	{
		PQExpBufferData fc_connstr;

		initPQExpBuffer(&fc_connstr);

		/*
		 * 强制目标 psql 的编码为 SQL_ASCII。我们并不知道数据库名称的编码是什么， 
		 * 这不重要，只要 psql 能够不变地将其转发到服务器即可。
		 */
		appendPQExpBufferStr(fc_buf, "\\encoding SQL_ASCII\n");
		appendPQExpBufferStr(fc_buf, "\\connect -reuse-previous=on ");

		appendPQExpBufferStr(&fc_connstr, "dbname=");
		appendConnStrVal(&fc_connstr, fc_dbname);

		/*
		 * 只要名称不包含换行符，SQL 标识符的引用就能满足 psql 元命令解析器。最好不要涉及 
		 * psql 解释的单引号，因为在 PostgreSQL 9.2 之前它们的行为有所不同。
		 */
		appendPQExpBufferStr(fc_buf, fmtIdEnc(fc_connstr.data, PG_SQL_ASCII));

		termPQExpBuffer(&fc_connstr);
	}
	else
	{
		appendPQExpBufferStr(fc_buf, "\\connect ");
		appendPQExpBufferStr(fc_buf, fmtIdEnc(fc_dbname, PG_SQL_ASCII));
	}
	appendPQExpBufferChar(fc_buf, '\n');
}


/*
 * 将一维 Postgres 数组的文本表示分解为各个项目。
 *
 * 成功时，返回 true，并将 *itemarray 和 *nitems 设置为描述一组字符串的数组。 
 * 在解析失败时，返回 false；*itemarray 可能存在或为 NULL。
 *
 * 注意：释放 itemarray 足以释放工作存储。
 */
bool parsePGArray(const char *fc_atext, char ***fc_itemarray, int *fc_nitems)
{
	int			fc_inputlen;
	char	  **fc_items;
	char	   *fc_strings;
	int			fc_curitem;

	/*
	 * 我们期望输入的形式为 "{item,item,item}"，其中任何项都是 
	 * 原始数据，或被双引号包围（在这种情况下，嵌入的字符包括反斜杠和引号都会被反斜杠转义）。
	 *
	 * 我们将结果构建为指向实际字符串数据的指针数组，所有内容都放在一个 malloc 块中，以方便释放。
	 * 最坏情况下的存储需求不超过每个输入字符一个指针和一个字符（考虑 "{,,,,,,,,,,}"）。
	 */
	*fc_itemarray = NULL;
	*fc_nitems = 0;
	fc_inputlen = strlen(fc_atext);
	if (fc_inputlen < 2 || fc_atext[0] != '{' || fc_atext[fc_inputlen - 1] != '}')
		return false;			/* 错误输入 */
	fc_items = (char **) malloc(fc_inputlen * (sizeof(char *) + sizeof(char)));
	if (fc_items == NULL)
		return false;			/* 内存不足 */
	*fc_itemarray = fc_items;
	fc_strings = (char *) (fc_items + fc_inputlen);

	fc_atext++;					/* 前进到初始 '{' */
	fc_curitem = 0;
	while (*fc_atext != '}')
	{
		if (*fc_atext == '\0')
			return false;		/* 字符串提前结束 */
		fc_items[fc_curitem] = fc_strings;
		while (*fc_atext != '}' && *fc_atext != ',')
		{
			if (*fc_atext == '\0')
				return false;	/* 字符串提前结束 */
			if (*fc_atext != '"')
				*fc_strings++ = *fc_atext++;	/* 复制未引用的数据 */
			else
			{
				/* 处理引用的子字符串 */
				fc_atext++;
				while (*fc_atext != '"')
				{
					if (*fc_atext == '\0')
						return false;	/* 字符串提前结束 */
					if (*fc_atext == '\\')
					{
						fc_atext++;
						if (*fc_atext == '\0')
							return false;	/* 字符串提前结束 */
					}
					*fc_strings++ = *fc_atext++;	/* 复制引用的数据 */
				}
				fc_atext++;
			}
		}
		*fc_strings++ = '\0';
		if (*fc_atext == ',')
			fc_atext++;
		fc_curitem++;
	}
	if (fc_atext[1] != '\0')
		return false;			/* 错误的语法（嵌入 '}'） */
	*fc_nitems = fc_curitem;
	return true;
}


/*
 * 将一个元素附加到一维 Postgres 数组的文本表示中。
 *
 * 调用者必须提供数组的初始 '{' 和结束 '}'。这个函数处理其他所有内容，包括逗号的插入 
 * 和值的引用。
 *
 * 我们假设 typdelim 为 ','。
 */
void appendPGArray(PQExpBuffer fc_buffer, const char *fc_value)
{
	bool		fc_needquote;
	const char *fc_tmp;

	if (fc_buffer->data[fc_buffer->len - 1] != '{')
		appendPQExpBufferChar(fc_buffer, ',');

	/* 决定我们是否需要引号；这应该与 array_out() 的选择匹配。 */
	if (fc_value[0] == '\0')
		fc_needquote = true;		/* 强制对空字符串使用引号 */
	else if (pg_strcasecmp(fc_value, "NULL") == 0)
		fc_needquote = true;		/* 强制对字面 NULL 使用引号 */
	else
		fc_needquote = false;

	if (!fc_needquote)
	{
		for (fc_tmp = fc_value; *fc_tmp; fc_tmp++)
		{
			char		fc_ch = *fc_tmp;

			if (fc_ch == '"' || fc_ch == '\\' ||
				fc_ch == '{' || fc_ch == '}' || fc_ch == ',' ||
			/* 这些与 array_isspace() 匹配： */
				fc_ch == ' ' || fc_ch == '\t' || fc_ch == '\n' ||
				fc_ch == '\r' || fc_ch == '\v' || fc_ch == '\f')
			{
				fc_needquote = true;
				break;
			}
		}
	}

	if (fc_needquote)
	{
		appendPQExpBufferChar(fc_buffer, '"');
		for (fc_tmp = fc_value; *fc_tmp; fc_tmp++)
		{
			char		fc_ch = *fc_tmp;

			if (fc_ch == '"' || fc_ch == '\\')
				appendPQExpBufferChar(fc_buffer, '\\');
			appendPQExpBufferChar(fc_buffer, fc_ch);
		}
		appendPQExpBufferChar(fc_buffer, '"');
	}
	else
		appendPQExpBufferStr(fc_buffer, fc_value);
}


/*
 * 格式化 reloptions 数组并将其附加到给定的缓冲区。
 *
 * "prefix" 被添加到选项名称前；通常是 "" 或 "toast."。
 *
 * 如果 reloptions 数组无法解析，则返回 false（在这种情况下将不会 
 * 追加任何内容到缓冲区），成功时返回 true。
 *
 * 注意：这一逻辑通常应该与后端的 flatten_reloptions() 匹配 
 * （在 adt/ruleutils.c 中）。
 */
bool appendReloptionsArray(PQExpBuffer fc_buffer, const char *fc_reloptions,
					  const char *fc_prefix, int fc_encoding, bool fc_std_strings)
{
	char	  **fc_options;
	int			fc_noptions;
	int			fc_i;

	if (!parsePGArray(fc_reloptions, &fc_options, &fc_noptions))
	{
		if (fc_options)
			free(fc_options);
		return false;
	}

	for (fc_i = 0; fc_i < fc_noptions; fc_i++)
	{
		char	   *fc_option = fc_options[fc_i];
		char	   *fc_name;
		char	   *fc_separator;
		char	   *fc_value;

		/*
		 * 每个数组元素应具有 name=value 的形式。如果 "=" 因某种原因缺失， 
		 * 则将其视为一个空值。
		 */
		fc_name = fc_option;
		fc_separator = strchr(fc_option, '=');
		if (fc_separator)
		{
			*fc_separator = '\0';
			fc_value = fc_separator + 1;
		}
		else
			fc_value = "";

		if (fc_i > 0)
			appendPQExpBufferStr(fc_buffer, ", ");
		appendPQExpBuffer(fc_buffer, "%s%s=", fc_prefix, fmtId(fc_name));

		/*
		 * 一般来说，我们需要引用该值；但为了避免不必要的
		 * 混乱，如果它是一个不需要引用的标识符则不引用。
		 * （我们也可以允许数字，但这比看起来更棘手——例如，前导零是否显著？
		 * 我们不想在这里对自定义 reloptions 的含义做出过多假设。）
		 */
		if (strcmp(fmtId(fc_value), fc_value) == 0)
			appendPQExpBufferStr(fc_buffer, fc_value);
		else
			appendStringLiteral(fc_buffer, fc_value, fc_encoding, fc_std_strings);
	}

	if (fc_options)
		free(fc_options);

	return true;
}


/*
 * processSQLNamePattern
 *
 * 扫描一个通配符模式字符串并生成适当的 WHERE 子句
 * 以限制返回的对象集。这些 WHERE 子句被附加到
 * buf 中已经部分构建的查询。返回是否添加了任何子句。
 *
 * conn：将发送查询的连接（用于转义规则的咨询）。
 * buf：输出参数。
 * pattern：用户指定的模式选项，若没有则为 NULL（暗含“*”）。
 * have_where：如果调用者已经发出了“WHERE”，则为 true
 * （子句将与现有的 WHERE 子句进行 AND 连接）。
 * force_escape：始终引用正则表达式特殊字符，即使在双引号外
 * （否则它们只在双引号之间被引用）。
 * schemavar：与模式名称匹配的查询变量名称。
 * 如果没有模式，可以为 NULL。
 * namevar：与对象名称模式匹配的查询变量名称。
 * altnamevar：NULL，或与名称匹配的替代变量名称。
 * visibilityrule：如果我们想限制可见对象的子句
 * （例如，“pg_catalog.pg_table_is_visible(p.oid)”）。可以为 NULL。
 * dbnamebuf：接收模式的数据库名称部分的输出参数（如果有）。可以为 NULL。
 * dotcnt：从模式解析出的分隔符数量，通过引用。
 *
 * 格式说明：buf 中已经存在的文本应以换行符结束。
 * 附加的文本（如果有）也将以换行符结束。
 */
bool processSQLNamePattern(PGconn *fc_conn, PQExpBuffer fc_buf, const char *fc_pattern,
					  bool fc_have_where, bool fc_force_escape,
					  const char *fc_schemavar, const char *fc_namevar,
					  const char *fc_altnamevar, const char *fc_visibilityrule,
					  PQExpBuffer fc_dbnamebuf, int *fc_dotcnt)
{
	PQExpBufferData fc_schemabuf;
	PQExpBufferData fc_namebuf;
	bool		fc_added_clause = false;
	int			fc_dcnt;

#define WHEREAND() \
	(appendPQExpBufferStr(fc_buf, fc_have_where ? "  AND " : "WHERE "), \
	 fc_have_where = true, fc_added_clause = true)

	if (fc_dotcnt == NULL)
		fc_dotcnt = &fc_dcnt;
	*fc_dotcnt = 0;
	if (fc_pattern == NULL)
	{
		/* 默认：选择所有可见对象 */
		if (fc_visibilityrule)
		{
			WHEREAND();
			appendPQExpBuffer(fc_buf, "%s\n", fc_visibilityrule);
		}
		return fc_added_clause;
	}

	initPQExpBuffer(&fc_schemabuf);
	initPQExpBuffer(&fc_namebuf);

	/*
	 * 将 shell 风格的 'pattern' 转换为我们想要执行的正则表达式。
	 * 对 SQL 字面量格式进行引用/转义将在下面通过
	 * appendStringLiteralConn() 完成。
	 *
	 * 如果调用者提供了 schemavar，我们想根据“.”拆分模式，
	 * 否则则不拆分。
	 */
	patternToSQLRegex(PQclientEncoding(fc_conn),
					  (fc_schemavar ? fc_dbnamebuf : NULL),
					  (fc_schemavar ? &fc_schemabuf : NULL),
					  &fc_namebuf,
					  fc_pattern, fc_force_escape, true, fc_dotcnt);

	/*
	 * 现在决定我们需要发出什么。我们可能在一个敌对的
	 * search_path 下运行，因此要限定每一个名称。
	 * 请注意，在任何情况下，模式中都会有一个前导“^（”
	 *
	 * 我们希望正则表达式匹配使用数据库的默认排序规则
	 * 在需要排序敏感行为的地方（例如，哪些字符
	 * 与 '\w' 匹配）。这在 PG v12 之前是默认行为，
	 * 但如果服务器是 >= v12，则需要通过显式的 COLLATE 子句强制它，
	 * 否则附加到“name”目录列的“C”排序规则将胜出。
	 */
	if (fc_namevar && fc_namebuf.len > 2)
	{
		/* 我们有一个名称模式，因此限制 namevar(s) */

		/* 优化掉一个 "*" 模式 */
		if (strcmp(fc_namebuf.data, "^(.*)$") != 0)
		{
			WHEREAND();
			if (fc_altnamevar)
			{
				appendPQExpBuffer(fc_buf,
								  "(%s OPERATOR(pg_catalog.~) ", fc_namevar);
				appendStringLiteralConn(fc_buf, fc_namebuf.data, fc_conn);
				if (PQserverVersion(fc_conn) >= 120000)
					appendPQExpBufferStr(fc_buf, " COLLATE pg_catalog.default");
				appendPQExpBuffer(fc_buf,
								  "\n        OR %s OPERATOR(pg_catalog.~) ",
								  fc_altnamevar);
				appendStringLiteralConn(fc_buf, fc_namebuf.data, fc_conn);
				if (PQserverVersion(fc_conn) >= 120000)
					appendPQExpBufferStr(fc_buf, " COLLATE pg_catalog.default");
				appendPQExpBufferStr(fc_buf, ")\n");
			}
			else
			{
				appendPQExpBuffer(fc_buf, "%s OPERATOR(pg_catalog.~) ", fc_namevar);
				appendStringLiteralConn(fc_buf, fc_namebuf.data, fc_conn);
				if (PQserverVersion(fc_conn) >= 120000)
					appendPQExpBufferStr(fc_buf, " COLLATE pg_catalog.default");
				appendPQExpBufferChar(fc_buf, '\n');
			}
		}
	}

	if (fc_schemavar && fc_schemabuf.len > 2)
	{
		/* 我们有一个模式模式，因此限制 schemavar */

		/* 优化掉一个 "*" 模式 */
		if (strcmp(fc_schemabuf.data, "^(.*)$") != 0 && fc_schemavar)
		{
			WHEREAND();
			appendPQExpBuffer(fc_buf, "%s OPERATOR(pg_catalog.~) ", fc_schemavar);
			appendStringLiteralConn(fc_buf, fc_schemabuf.data, fc_conn);
			if (PQserverVersion(fc_conn) >= 120000)
				appendPQExpBufferStr(fc_buf, " COLLATE pg_catalog.default");
			appendPQExpBufferChar(fc_buf, '\n');
		}
	}
	else
	{
		/* 没有给出模式，因此只选择可见对象 */
		if (fc_visibilityrule)
		{
			WHEREAND();
			appendPQExpBuffer(fc_buf, "%s\n", fc_visibilityrule);
		}
	}

	termPQExpBuffer(&fc_schemabuf);
	termPQExpBuffer(&fc_namebuf);

	return fc_added_clause;
#undef WHEREAND
}

/*
 * 将可能带有名称空间的壳样式对象名称模式转换为最多
 * 三个 SQL 样式的正则表达式，转换引号，将未引用的字母转换为小写，
 * 并将壳样式通配符字符调整为正则表达式符号。
 *
 * 如果 dbnamebuf 和 schemabuf 参数均非 NULL，并且模式
 * 包含两个或更多的 dbname/schema/name 分隔符，则我们将
 * 第一个和第二个分隔符之前的模式部分解析到 dbnamebuf 和
 * schemabuf 中，其余部分解析到 namebuf 中。
 *
 * 如果 dbnamebuf 为 NULL 而 schemabuf 非 NULL，并且模式至少包含
 * 一个分隔符，则我们将第一部分解析到 schemabuf 中，其余部分
 * 解析到 namebuf 中。
 *
 * 否则，我们将所有模式解析到 namebuf 中。
 *
 * 如果模式中包含的点式部分比可解析的缓冲区多，则多余的点
 * 将被视为字面字符并写入 namebuf 中，尽管它们将被计数。
 * 调用者应始终检查通过引用返回的 dotcnt 的值，并适当处理
 * 此错误情况。
 *
 * 我们用 "^(...)$" 将正则表达式包围起来，以强制它们匹配整个字符串，
 * 根据 SQL 的惯例。我们必须使用括号，以防字符串包含 "|",
 * 否则 "^" 和 "$" 将绑定到第一个和最后一个替代项，
 * 这不是我们想要的。是否为 dbnamebuf 执行此操作由 want_literal_dbname 参数控制。
 *
 * 我们解析到缓冲区的正则表达式将附加到已经存在的数据中（如果有的话）。
 * 如果我们解析的字段数少于给定的缓冲区数，则多余的缓冲区将保持不变。
 *
 * 编码：给定模式的字符编码
 * dbnamebuf：输出参数接收模式的数据库名称部分（如果有）。可以为 NULL。
 * schemabuf：输出参数接收模式的模式名称部分（如果有）。可以为 NULL。
 * namebuf：输出参数接收模式的数据库名称部分（如果有）。可以为 NULL。
 * pattern：用户指定的模式选项，或在没有指定时为 NULL（隐含为 "*"）。
 * force_escape：始终对正则表达式特殊字符进行引号处理，即使在双引号外部（否则它们仅在双引号之间被引号处理）。
 * want_literal_dbname：如果为真，则模式的数据库名称部分中的正则表达式特殊字符将不会被转义，
 * 并且 dbname 也不会被转换为正则表达式。
 * dotcnt：输出参数接收从模式中解析的分隔符数量。
 */
void patternToSQLRegex(int fc_encoding, PQExpBuffer fc_dbnamebuf, PQExpBuffer fc_schemabuf,
				  PQExpBuffer fc_namebuf, const char *fc_pattern, bool fc_force_escape,
				  bool fc_want_literal_dbname, int *fc_dotcnt)
{
	PQExpBufferData fc_buf[3];
	PQExpBufferData fc_left_literal;
	PQExpBuffer fc_curbuf;
	PQExpBuffer fc_maxbuf;
	int			fc_i;
	bool		fc_inquotes;
	bool		fc_left;
	const char *fc_cp;

	Assert(fc_pattern != NULL);
	Assert(fc_namebuf != NULL);

	/* 调用者永远不应期望 "dbname.relname" 格式 */
	Assert(fc_dbnamebuf == NULL || fc_schemabuf != NULL);
	Assert(fc_dotcnt != NULL);

	*fc_dotcnt = 0;
	fc_inquotes = false;
	fc_cp = fc_pattern;

	if (fc_dbnamebuf != NULL)
		fc_maxbuf = &fc_buf[2];
	else if (fc_schemabuf != NULL)
		fc_maxbuf = &fc_buf[1];
	else
		fc_maxbuf = &fc_buf[0];

	fc_curbuf = &fc_buf[0];
	if (fc_want_literal_dbname)
	{
		fc_left = true;
		initPQExpBuffer(&fc_left_literal);
	}
	else
		fc_left = false;
	initPQExpBuffer(fc_curbuf);
	appendPQExpBufferStr(fc_curbuf, "^(");
	while (*fc_cp)
	{
		char		fc_ch = *fc_cp;

		if (fc_ch == '"')
		{
			if (fc_inquotes && fc_cp[1] == '"')
			{
				/* 发出一个引号，保持在引号模式中 */
				appendPQExpBufferChar(fc_curbuf, '"');
				if (fc_left)
					appendPQExpBufferChar(&fc_left_literal, '"');
				fc_cp++;
			}
			else
				fc_inquotes = !fc_inquotes;
			fc_cp++;
		}
		else if (!fc_inquotes && isupper((unsigned char) fc_ch))
		{
			appendPQExpBufferChar(fc_curbuf,
								  pg_tolower((unsigned char) fc_ch));
			if (fc_left)
				appendPQExpBufferChar(&fc_left_literal,
									  pg_tolower((unsigned char) fc_ch));
			fc_cp++;
		}
		else if (!fc_inquotes && fc_ch == '*')
		{
			appendPQExpBufferStr(fc_curbuf, ".*");
			if (fc_left)
				appendPQExpBufferChar(&fc_left_literal, '*');
			fc_cp++;
		}
		else if (!fc_inquotes && fc_ch == '?')
		{
			appendPQExpBufferChar(fc_curbuf, '.');
			if (fc_left)
				appendPQExpBufferChar(&fc_left_literal, '?');
			fc_cp++;
		}
		else if (!fc_inquotes && fc_ch == '.')
		{
			fc_left = false;
			if (fc_dotcnt)
				(*fc_dotcnt)++;
			if (fc_curbuf < fc_maxbuf)
			{
				appendPQExpBufferStr(fc_curbuf, ")$");
				fc_curbuf++;
				initPQExpBuffer(fc_curbuf);
				appendPQExpBufferStr(fc_curbuf, "^(");
				fc_cp++;
			}
			else
				appendPQExpBufferChar(fc_curbuf, *fc_cp++);
		}
		else if (fc_ch == '$')
		{
			/*
			 * 美元符号总是被引用，无论是在引号内还是不在引号内。
			 * 原因是它在SQL标识符中是允许的，因此有重要的用例
			 * 将其视为字面量，同时由于我们自动锚定模式，
			 * 没有用例需要它拥有其正则表达式的意义。
			 */
			appendPQExpBufferStr(fc_curbuf, "\\$");
			if (fc_left)
				appendPQExpBufferChar(&fc_left_literal, '$');
			fc_cp++;
		}
		else
		{
			/*
			 * 普通数据字符，传输到模式
			 *
			 * 在双引号内，或者在force_escape为true的情况下，
			 * 用反斜杠引用正则表达式特殊字符以避免正则表达式错误。
			 * 然而在引号外，让它们原样通过；
			 * 这允许有知识的用户构建比shell风格模式更强大的正则表达式。
			 *
			 * 不过，作为例外，总是引用"[]"，
			 * 因为这更可能是尝试写数组类型名称，而不是正则表达式括号表达式的开始。
			 */
			if ((fc_inquotes || fc_force_escape) &&
				strchr("|*+?()[]{}.^$\\", fc_ch))
				appendPQExpBufferChar(fc_curbuf, '\\');
			else if (fc_ch == '[' && fc_cp[1] == ']')
				appendPQExpBufferChar(fc_curbuf, '\\');
			fc_i = PQmblenBounded(fc_cp, fc_encoding);
			while (fc_i--)
			{
				if (fc_left)
					appendPQExpBufferChar(&fc_left_literal, *fc_cp);
				appendPQExpBufferChar(fc_curbuf, *fc_cp++);
			}
		}
	}
	appendPQExpBufferStr(fc_curbuf, ")$");

	if (fc_namebuf)
	{
		appendPQExpBufferStr(fc_namebuf, fc_curbuf->data);
		termPQExpBuffer(fc_curbuf);
		fc_curbuf--;
	}

	if (fc_schemabuf && fc_curbuf >= fc_buf)
	{
		appendPQExpBufferStr(fc_schemabuf, fc_curbuf->data);
		termPQExpBuffer(fc_curbuf);
		fc_curbuf--;
	}

	if (fc_dbnamebuf && fc_curbuf >= fc_buf)
	{
		if (fc_want_literal_dbname)
			appendPQExpBufferStr(fc_dbnamebuf, fc_left_literal.data);
		else
			appendPQExpBufferStr(fc_dbnamebuf, fc_curbuf->data);
		termPQExpBuffer(fc_curbuf);
	}

	if (fc_want_literal_dbname)
		termPQExpBuffer(&fc_left_literal);
}
